package nvd

import (
	"encoding/json"
	"fmt"
	"runtime"
	"strconv"
	"strings"
	"time"

	"github.com/hashicorp/go-version"
	"github.com/k0kubun/pp"
	"github.com/spf13/viper"
	c "github.com/vulsio/go-cve-dictionary/config"
	"github.com/vulsio/go-cve-dictionary/fetcher"
	"github.com/vulsio/go-cve-dictionary/log"
	"github.com/vulsio/go-cve-dictionary/models"
	"github.com/vulsio/go-cve-dictionary/util"
	"golang.org/x/xerrors"
)

// makeNvdMetaURLs returns a URL of NVD Feed
func makeNvdMetaURLs(year int) (url []string) {
	formatTemplate := "https://nvd.nist.gov/feeds/json/cve/1.1/nvdcve-1.1-%s.meta"
	if year == c.Latest {
		for _, name := range []string{"modified", "recent"} {
			url = append(url, fmt.Sprintf(formatTemplate, name))
		}
	} else {
		feed := strconv.Itoa(year)
		url = append(url, fmt.Sprintf(formatTemplate, feed))
	}
	return
}

// FetchConvert Fetch CVE vulnerability information from NVD
func FetchConvert(uniqCve map[string]map[string]models.Nvd, years []string) error {
	for _, y := range years {
		items, err := fetch(y)
		if err != nil {
			return xerrors.Errorf("Failed to fetch. err: %w", err)
		}

		cves, err := convert(items)
		if err != nil {
			return xerrors.Errorf("Failed to convert. err: %w", err)
		}
		distributeCvesByYear(uniqCve, cves)
	}
	return nil
}

func fetch(year string) ([]CveItem, error) {
	url, err := models.GetURLByYear(models.NvdType, year)
	if err != nil {
		return nil, xerrors.Errorf("Failed to GetURLByYear. err: %w", err)
	}

	body, err := fetcher.FetchFeedFile(url, true)
	if err != nil {
		return nil, xerrors.Errorf("Failed to fetch. err: %w", err)
	}

	var nvd, nvdIncludeRejectedCve Nvd
	if err = json.Unmarshal(body, &nvdIncludeRejectedCve); err != nil {
		return nil, xerrors.Errorf("Failed to unmarshal. url: %s, err: %w", url, err)
	}

	nvd.CveDataType = nvdIncludeRejectedCve.CveDataType
	nvd.CveDataFormat = nvdIncludeRejectedCve.CveDataFormat
	nvd.CveDataVersion = nvdIncludeRejectedCve.CveDataVersion
	nvd.CveDataNumberOfCVEs = nvdIncludeRejectedCve.CveDataNumberOfCVEs
	nvd.CveDataTimestamp = nvdIncludeRejectedCve.CveDataTimestamp

	// Remove rejected CVEs
	for i, item := range nvdIncludeRejectedCve.CveItems {
		for _, description := range item.Cve.Description.DescriptionData {
			if !(strings.Contains(description.Value, "** REJECT **")) {
				nvd.CveItems = append(nvd.CveItems, nvdIncludeRejectedCve.CveItems[i])
			}
		}
	}

	return nvd.CveItems, nil
}

func convert(items []CveItem) (map[string]models.Nvd, error) {
	reqChan := make(chan CveItem, len(items))
	resChan := make(chan *models.Nvd, len(items))
	errChan := make(chan error, len(items))
	defer close(reqChan)
	defer close(resChan)
	defer close(errChan)

	go func() {
		for _, item := range items {
			reqChan <- item
		}
	}()

	concurrency := runtime.NumCPU() + 2
	tasks := util.GenWorkers(concurrency)
	for range items {
		tasks <- func() {
			req := <-reqChan
			cve, err := convertToModel(&req)
			if err != nil {
				errChan <- err
				return
			}
			resChan <- cve
		}
	}

	cves := map[string]models.Nvd{}
	errs := []error{}
	timeout := time.After(10 * 60 * time.Second)
	for range items {
		select {
		case res := <-resChan:
			cves[res.CveID] = *res
		case err := <-errChan:
			errs = append(errs, err)
		case <-timeout:
			return nil, fmt.Errorf("Timeout Fetching")
		}
	}
	if 0 < len(errs) {
		return nil, xerrors.Errorf("%w", errs)
	}
	return cves, nil
}

// Nvd is a struct of NVD JSON
// https://scap.nist.gov/schema/nvd/feed/1.1/nvd_cve_feed_json_1.1.schema
type Nvd struct {
	CveDataType         string    `json:"CVE_data_type"`
	CveDataFormat       string    `json:"CVE_data_format"`
	CveDataVersion      string    `json:"CVE_data_version"`
	CveDataNumberOfCVEs string    `json:"CVE_data_numberOfCVEs"`
	CveDataTimestamp    string    `json:"CVE_data_timestamp"`
	CveItems            []CveItem `json:"CVE_Items"`
}

// CveItem is a struct of Nvd>CveItems
type CveItem struct {
	Cve struct {
		DataType    string `json:"data_type"`
		DataFormat  string `json:"data_format"`
		DataVersion string `json:"data_version"`
		CveDataMeta struct {
			ID       string `json:"ID"`
			ASSIGNER string `json:"ASSIGNER"`
		} `json:"CVE_data_meta"`
		Problemtype struct {
			ProblemtypeData []struct {
				Description []struct {
					Lang  string `json:"lang"`
					Value string `json:"value"`
				} `json:"description"`
			} `json:"problemtype_data"`
		} `json:"problemtype"`
		References struct {
			ReferenceData []struct {
				URL       string   `json:"url"`
				Name      string   `json:"name"`
				RefSource string   `json:"refsource"`
				Tags      []string `json:"tags"`
			} `json:"reference_data"`
		} `json:"references"`
		Description struct {
			DescriptionData []struct {
				Lang  string `json:"lang"`
				Value string `json:"value"`
			} `json:"description_data"`
		} `json:"description"`
	} `json:"cve"`
	Configurations struct {
		CveDataVersion string `json:"CVE_data_version"`
		Nodes          []struct {
			Operator string `json:"operator"`
			Negate   bool   `json:"negate"`
			Cpes     []struct {
				Vulnerable            bool   `json:"vulnerable"`
				Cpe23URI              string `json:"cpe23Uri"`
				VersionStartExcluding string `json:"versionStartExcluding"`
				VersionStartIncluding string `json:"versionStartIncluding"`
				VersionEndExcluding   string `json:"versionEndExcluding"`
				VersionEndIncluding   string `json:"versionEndIncluding"`
			} `json:"cpe_match"`
			Children []struct {
				Operator string `json:"operator"`
				Cpes     []struct {
					Vulnerable            bool   `json:"vulnerable"`
					Cpe23URI              string `json:"cpe23Uri"`
					VersionStartExcluding string `json:"versionStartExcluding"`
					VersionStartIncluding string `json:"versionStartIncluding"`
					VersionEndExcluding   string `json:"versionEndExcluding"`
					VersionEndIncluding   string `json:"versionEndIncluding"`
				} `json:"cpe_match"`
			} `json:"children,omitempty"`
		} `json:"nodes"`
	} `json:"configurations"`
	Impact struct {
		BaseMetricV3 struct {
			CvssV3 struct {
				Version               string  `json:"version"`
				VectorString          string  `json:"vectorString"`
				AttackVector          string  `json:"attackVector"`
				AttackComplexity      string  `json:"attackComplexity"`
				PrivilegesRequired    string  `json:"privilegesRequired"`
				UserInteraction       string  `json:"userInteraction"`
				Scope                 string  `json:"scope"`
				ConfidentialityImpact string  `json:"confidentialityImpact"`
				IntegrityImpact       string  `json:"integrityImpact"`
				AvailabilityImpact    string  `json:"availabilityImpact"`
				BaseScore             float64 `json:"baseScore"`
				BaseSeverity          string  `json:"baseSeverity"`
			} `json:"cvssV3"`
			ExploitabilityScore float64 `json:"exploitabilityScore"`
			ImpactScore         float64 `json:"impactScore"`
		} `json:"baseMetricV3"`
		BaseMetricV2 struct {
			CvssV2 struct {
				Version               string  `json:"version"`
				VectorString          string  `json:"vectorString"`
				AccessVector          string  `json:"accessVector"`
				AccessComplexity      string  `json:"accessComplexity"`
				Authentication        string  `json:"authentication"`
				ConfidentialityImpact string  `json:"confidentialityImpact"`
				IntegrityImpact       string  `json:"integrityImpact"`
				AvailabilityImpact    string  `json:"availabilityImpact"`
				BaseScore             float64 `json:"baseScore"`
			} `json:"cvssV2"`
			Severity                string  `json:"severity"`
			ExploitabilityScore     float64 `json:"exploitabilityScore"`
			ImpactScore             float64 `json:"impactScore"`
			ObtainAllPrivilege      bool    `json:"obtainAllPrivilege"`
			ObtainUserPrivilege     bool    `json:"obtainUserPrivilege"`
			ObtainOtherPrivilege    bool    `json:"obtainOtherPrivilege"`
			UserInteractionRequired bool    `json:"userInteractionRequired"`
		} `json:"baseMetricV2"`
	} `json:"impact"`
	PublishedDate    string `json:"publishedDate"`
	LastModifiedDate string `json:"lastModifiedDate"`
}

// convertToModel converts Nvd JSON to model structure.
func convertToModel(item *CveItem) (*models.Nvd, error) {
	//References
	refs := []models.NvdReference{}
	for _, r := range item.Cve.References.ReferenceData {
		ref := models.NvdReference{
			Reference: models.Reference{
				Link:   r.URL,
				Name:   r.Name,
				Source: r.RefSource,
				Tags:   strings.Join(r.Tags, ","),
			},
		}
		refs = append(refs, ref)
	}

	// Certs
	certs := []models.NvdCert{}
	for _, ref := range item.Cve.References.ReferenceData {
		if !strings.HasPrefix(ref.URL, "http") {
			continue
		}
		if strings.Contains(ref.URL, "us-cert") {
			ss := strings.Split(ref.URL, "/")
			title := fmt.Sprintf("US-CERT-%s", ss[len(ss)-1])
			certs = append(certs, models.NvdCert{
				Cert: models.Cert{
					Link:  ref.URL,
					Title: title,
				},
			})
		}
	}

	// Cwes
	cwes := []models.NvdCwe{}
	for _, data := range item.Cve.Problemtype.ProblemtypeData {
		for _, desc := range data.Description {
			cwes = append(cwes, models.NvdCwe{
				CweID: desc.Value,
			})
		}
	}

	full := viper.GetBool("full")
	cpes := []models.NvdCpe{}
	for _, node := range item.Configurations.Nodes {
		if node.Negate {
			continue
		}

		nodeCpes := []models.NvdCpe{}
		for _, cpe := range node.Cpes {
			// if !cpe.Vulnerable {
			// CVE-2017-14492 and CVE-2017-8581 has a cpe that has vulnerable:false.
			// But these vulnerable: false cpe is also vulnerable...
			// So, ignore the vulnerable flag of this layer(under nodes>cpe)
			// }
			cpeBase, err := fetcher.ParseCpeURI(cpe.Cpe23URI)
			if err != nil {
				// logging only
				log.Infof("Failed to parse CpeURI %s: %s", cpe.Cpe23URI, err)
				continue
			}
			cpeBase.VersionStartExcluding = cpe.VersionStartExcluding
			cpeBase.VersionStartIncluding = cpe.VersionStartIncluding
			cpeBase.VersionEndExcluding = cpe.VersionEndExcluding
			cpeBase.VersionEndIncluding = cpe.VersionEndIncluding
			nodeCpes = append(nodeCpes, models.NvdCpe{
				CpeBase: *cpeBase,
				EnvCpes: []models.NvdEnvCpe{},
			})
			if !checkIfVersionParsable(cpeBase) {
				return nil, fmt.Errorf(
					"Version parse err. Please add a issue on [GitHub](https://github.com/vulsio/go-cve-dictionary/issues/new). Title: %s, Content:%s",
					item.Cve.CveDataMeta.ID,
					pp.Sprintf("%v", *item),
				)
			}
		}
		for _, child := range node.Children {
			for _, cpe := range child.Cpes {
				if cpe.Vulnerable {
					cpeBase, err := fetcher.ParseCpeURI(cpe.Cpe23URI)
					if err != nil {
						return nil, err
					}
					cpeBase.VersionStartExcluding = cpe.VersionStartExcluding
					cpeBase.VersionStartIncluding = cpe.VersionStartIncluding
					cpeBase.VersionEndExcluding = cpe.VersionEndExcluding
					cpeBase.VersionEndIncluding = cpe.VersionEndIncluding
					nodeCpes = append(nodeCpes, models.NvdCpe{
						CpeBase: *cpeBase,
						EnvCpes: []models.NvdEnvCpe{},
					})
					if !checkIfVersionParsable(cpeBase) {
						return nil, fmt.Errorf(
							"Version parse err. Please add a issue on [GitHub](https://github.com/vulsio/go-cve-dictionary/issues/new). Title: %s, Content:%s",
							item.Cve.CveDataMeta.ID,
							pp.Sprintf("%v", *item),
						)
					}
				} else {
					if full && node.Operator == "AND" {
						for i, c := range nodeCpes {
							cpeBase, err := fetcher.ParseCpeURI(cpe.Cpe23URI)
							if err != nil {
								return nil, err
							}
							cpeBase.VersionStartExcluding = cpe.VersionStartExcluding
							cpeBase.VersionStartIncluding = cpe.VersionStartIncluding
							cpeBase.VersionEndExcluding = cpe.VersionEndExcluding
							cpeBase.VersionEndIncluding = cpe.VersionEndIncluding
							nodeCpes[i].EnvCpes = append(c.EnvCpes, models.NvdEnvCpe{
								CpeBase: *cpeBase,
							})

							if !checkIfVersionParsable(cpeBase) {
								return nil, fmt.Errorf(
									"Please add a issue on [GitHub](https://github.com/vulsio/go-cve-dictionary/issues/new). Title: Version parse err: %s, Content:%s",
									item.Cve.CveDataMeta.ID,
									pp.Sprintf("%v", *item),
								)
							}
						}
					}
				}
			}
		}
		cpes = append(cpes, nodeCpes...)
	}

	// Description
	descs := []models.NvdDescription{}
	for _, desc := range item.Cve.Description.DescriptionData {
		descs = append(descs, models.NvdDescription{
			Lang:  desc.Lang,
			Value: desc.Value,
		})
	}

	publish, err := parseNvdTime(item.PublishedDate)
	if err != nil {
		return nil, err
	}
	modified, err := parseNvdTime(item.LastModifiedDate)
	if err != nil {
		return nil, err
	}
	c2 := item.Impact.BaseMetricV2
	c3 := item.Impact.BaseMetricV3

	return &models.Nvd{
		CveID:        item.Cve.CveDataMeta.ID,
		Descriptions: descs,
		Cvss2: models.NvdCvss2Extra{
			Cvss2: models.Cvss2{
				VectorString:          c2.CvssV2.VectorString,
				AccessVector:          c2.CvssV2.AccessVector,
				AccessComplexity:      c2.CvssV2.AccessComplexity,
				Authentication:        c2.CvssV2.Authentication,
				ConfidentialityImpact: c2.CvssV2.ConfidentialityImpact,
				IntegrityImpact:       c2.CvssV2.IntegrityImpact,
				AvailabilityImpact:    c2.CvssV2.AvailabilityImpact,
				BaseScore:             c2.CvssV2.BaseScore,
				Severity:              c2.Severity,
			},
			ExploitabilityScore:     c2.ExploitabilityScore,
			ImpactScore:             c2.ImpactScore,
			ObtainAllPrivilege:      c2.ObtainAllPrivilege,
			ObtainUserPrivilege:     c2.ObtainUserPrivilege,
			ObtainOtherPrivilege:    c2.ObtainOtherPrivilege,
			UserInteractionRequired: c2.UserInteractionRequired,
		},
		Cvss3: models.NvdCvss3{
			Cvss3: models.Cvss3{
				VectorString:          c3.CvssV3.VectorString,
				AttackVector:          c3.CvssV3.AttackVector,
				AttackComplexity:      c3.CvssV3.AttackComplexity,
				PrivilegesRequired:    c3.CvssV3.PrivilegesRequired,
				UserInteraction:       c3.CvssV3.UserInteraction,
				Scope:                 c3.CvssV3.Scope,
				ConfidentialityImpact: c3.CvssV3.ConfidentialityImpact,
				IntegrityImpact:       c3.CvssV3.IntegrityImpact,
				AvailabilityImpact:    c3.CvssV3.AvailabilityImpact,
				BaseScore:             c3.CvssV3.BaseScore,
				BaseSeverity:          c3.CvssV3.BaseSeverity,
				ExploitabilityScore:   c3.ExploitabilityScore,
				ImpactScore:           c3.ImpactScore,
			},
		},
		Cwes:             cwes,
		Cpes:             cpes,
		References:       refs,
		Certs:            certs,
		PublishedDate:    publish,
		LastModifiedDate: modified,
	}, nil
}

func checkIfVersionParsable(cpeBase *models.CpeBase) bool {
	if cpeBase.Version != "ANY" && cpeBase.Version != "NA" {
		vers := []string{cpeBase.VersionStartExcluding,
			cpeBase.VersionStartIncluding,
			cpeBase.VersionEndIncluding,
			cpeBase.VersionEndExcluding}
		for _, v := range vers {
			if v == "" {
				continue
			}
			v := strings.Replace(v, `\`, "", -1)
			if _, err := version.NewVersion(v); err != nil {
				return false
			}
		}
	}
	return true
}

func parseNvdTime(strtime string) (t time.Time, err error) {
	layout := "2006-01-02T15:04Z"
	t, err = time.Parse(layout, strtime)
	if err != nil {
		return t, xerrors.Errorf("Failed to parse time, time: %s, err: %w", strtime, err)
	}
	return
}

func distributeCvesByYear(uniqCves map[string]map[string]models.Nvd, cves map[string]models.Nvd) {
	for _, cve := range cves {
		y := strings.Split(cve.CveID, "-")[1]
		if _, ok := uniqCves[y]; !ok {
			uniqCves[y] = map[string]models.Nvd{}
		}
		if destCve, ok := uniqCves[y][cve.CveID]; !ok {
			uniqCves[y][cve.CveID] = cve
		} else {
			if cve.LastModifiedDate.After(destCve.LastModifiedDate) {
				uniqCves[y][cve.CveID] = cve
			}
		}
	}
}
